Skip to content

好友关注

要点:

  • Feed流的使用

1.1 关注和取关

1.1.1 需求

image-20260419170951528

基于该表结构,需要实现两个接口:

  • 关注和取关接口

  • 判断是否关注的接口

    关注是User之间关系,博主和粉丝是多对多的关系,一个博主可以有多个粉丝,一个粉丝也可以关注多个博主,所以抽离出来一张 tb_follow 表来表示:

    image-20260419171430062

1.2 共同关注

代码:

java
@Override
public Result followCommons(Long id) {
    // 1. 获取当前登录用户ID
    UserDTO dto = UserHolder.getUser();
    if (dto == null) {
        return Result.fail("请先登录");
    }
    Long userId = dto.getId();

    // 2. 使用 Set 求博主和当前登录用户关注的共同用户
    String bloggerKey = "follow:" + id;
    String userKey = "follow:" + userId;
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(bloggerKey, userKey);
    if (intersect == null || intersect.isEmpty()) {
        return Result.ok(Collections.emptyList());
    }
    List<Long> idList = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
    List<UserDTO> dtoList = userService.listByIds(idList)
            .stream()
            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
            .collect(Collectors.toList());
    return Result.ok(dtoList);
}

1.3 关注推送

1.3.1 背景

  1. 关注推送也叫做Feed流,直译为投喂,为用户持续提供“沉浸式”的体验,通过无限下拉刷新获取新消息。

    image-20260420092903867

  2. Feed流模式

    image-20260420093500757

  3. Feed流实现方案

    • 拉模式:也叫读扩散,用户拉取Feed流,优点是节省内存空间,缺点是延迟高,时间换空间

      image-20260420094015591

    • 推模式:也叫写扩散,将Feed流写到关联用户的收件箱中,优点是延迟低,缺点是重复写占用空间,空间换时间

      image-20260420094117224

    • 推拉结合模式:大致原则是优化用户的体验,达到延迟低的效果,对于普通用户发的消息,直接使用推模式,此类用户占用内存较少;对于大V发的消息,对大V的活跃粉丝使用推模式,对大V的普通粉丝使用拉模式。

      image-20260420094415333

    • 三种模式对比

      image-20260420094740790

      本例使用推模式。

1.3.2 代码实现

  1. 需求

    image-20260420095127203

  2. 实现思路

    • Feed流分页查询 传统分页是指基于角标访问,如 ZSet Range函数,在数据插入时,数据的角标会有变化,导致分页困难

      image-20260420204013956

    • 滚动查询实现基本思路

      ZREVRANGEBYSCORE key max min [WITHSCORES] [LIMIT offset count] :根据分数范围查询集合,max为分数最大值,min为分数最小值,降序排列

      image-20260421143938050

      先执行第一条命令,1000改为当前时间戳,查询出时间最靠前的几条记录,然后记录当前查询记录中的最小值,以该条记录为基准,再进行下一次的查询

    • 滚动查询的参数问题

      image-20260421150126548

      count是与前端约定好了的,offset的值取决于你在“当前这页结果”中,查到了几个与最小值重复的元素,并且要加一个判断逻辑:

      • 如果本次查询的最小分数 == 上次的分数,则 nextOffset = lastOffset + count

      • 如果分数变了,则 nextOffset = count

      (这里的 count 是指本次结果中与最小分数相同的元素个数)

  3. 具体实现

    • 接口信息:

    image-20260421150526260

    • 代码:

      java
          public Result queryBlogOfFollow(Long max, Integer offset) {
              // 1. 获取当前用户
              UserDTO dto = UserHolder.getUser();
              if (dto == null) {
                  return Result.fail("请先登录!");
              }
              Long userId = dto.getId();
              // 2. 查询收件箱
              String key = FEED_KEY + userId;
              // 3. ZREVRANGEBYSCORE key max 0 WITHSCORES LIMIT offset 3
              Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
              if (typedTuples == null || typedTuples.isEmpty()) {
                  return Result.ok();
              }
              // 4. 解析数据 blogId 时间戳
              List<Long> blogs = new ArrayList<>(typedTuples.size());
              long minTime = 0L;
              int offsetIndex = 1;
              for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
                  blogs.add(Long.valueOf(typedTuple.getValue()));
                  // 时间戳
                  long time = typedTuple.getScore().longValue();
                  if (minTime == time) {
                      // 如果和最小值时间戳重复,偏移量 + 1
                      offsetIndex++;
                  } else {
                      // 不一样说明当前时间戳不是最小值,偏移量重新数
                      minTime = time;
                      offsetIndex = 1;
                  }
              }
              if (minTime == max){
                  offsetIndex += offset;
              }
              // 5. 根据获得的 blogId 查询博客
              String idStr = StrUtil.join(",", blogs);
              List<Blog> blogList = query().in("id", blogs).last("ORDER BY FIELD(id," + idStr + ")").list();
      
              blogList.forEach(blog -> {
                  // 5.1 查询博客用户信息
                  queryBlogUser(blog);
                  // 5.2 查询博客是否被点赞
                  isBlogLiked(blog);
              });
      
              // 6. 封装结果返回
              ScrollResult scrollResult = new ScrollResult();
              scrollResult.setOffset(offsetIndex);
              scrollResult.setList(blogList);
              scrollResult.setMinTime(minTime);
              return Result.ok(scrollResult);
          }